import gtk
from gtkmvc import Controller
import gtkmvc

from model_progress import MyModelProgress
from view_progress import MyViewProgress
from ctrl_progress import MyControllerProgress

import gmaps
import downloader
from worker import Progress

import time
import re

key = "ABQIAAAACz69gin6BxJBW21hNzxwBhT2yXp_ZAY8_ufC3CFXhHIE1NvwkxQjo0KGnh3InyeP5gr9wPZZSnmaow"

prefix = {
    -4: "p",
    -3: "n",
    -2: "u",
    -1: "m",
    0: "",
    1: "k",
    2: "M",
    3: "G",
    4: "T",
}

# http://faq.pygtk.org/index.py?req=show&file=faq03.007.htp
def service_gui():
    time.sleep(0.01)
    while gtk.events_pending():
        gtk.main_iteration(False)


def parse_angle(text):
    """Function to parse angle roughly matching dms format.
    http://code.google.com/p/biogeomancer-core/wiki/LatLongParsingRequirements
    """

    text = text.decode('utf8')

    decimal_pattern = ur"(?: \d+ (?: \.\d*)? | \.\d+)"
    deg_pattern = ur"(?: [d\u00b0] | deg)"
    min_pattern = ur"(?: [m'] | min)"
    sec_pattern = ur"(?: [s\"] | sec)"
    dir_pattern = ur"[nsew]"

    # First match against plain decimal number.
    result = re.search(
        ur"^\s* ([\-\+]? \s* %s) \s*$" % decimal_pattern,
        text,
        re.X | re.S)
    if result:
        return float(result.group(1))

    # Next try d-m-s notation.
    result = re.search(
        ur"""^ \s*
            ([\-\+])? \s*
            (%s) \s* %s \s*
            (?: (%s) \s* %s)? \s*
            (?: (%s) \s* %s?)? \s*
            (?: (%s) \s*)? $""" % (
            decimal_pattern, deg_pattern,
            decimal_pattern, min_pattern,
            decimal_pattern, sec_pattern,
            dir_pattern),
        text,
        re.X | re.S | re.I)
    if result:
        (sign, deg, min, sec, dir) = result.groups()
        deg = parse_float(deg)
        min = parse_float(min)
        sec = parse_float(sec)
        # Build value from components.
        value = deg + min / 60.0 + sec / (60.0 * 60.0)
        # Check if sign needs to be inverted.
        if sign == '-':
            value *= -1
        if dir in ['S', 's', 'W', 'w']:
            value *= -1
        return value

    return None

def extract_pano_id_from_url(url):
    result = re.search(ur"[\&\?] panoid \= (\w+)", url, re.X | re.I)
    if result:
        return result.group(1)
    else:
        return None

def extract_lat_long_from_url(url):
    # Try looking for cbll, then ll, then sll in URL.
    # (streetview latlng, latlng, search latlng)
    params = ['cbll', 'll', 'sll']
    generic = ur"[\&\?] %s \= (\-?\d+ (?: \.\d+)?) , (\-?\d+ (?: \.\d+)?)"
    patterns = [generic % x for x in params]
    for pattern in patterns:
        result = re.search(pattern, url, re.X | re.I)
        if result:
            latitude = float(result.group(1))
            longitude = float(result.group(2))
            return gmaps.LatLng(latitude, longitude)
    return None


class MyController(Controller):
    
    def __init__(self, model, view):
        Controller.__init__(self, model, view)

    def register_view(self, view):
        view['main_window'].connect('destroy', gtk.main_quit)

    def register_adapters(self):
        pass

    # ----------------------------------------------------------------
    # GUI signal handlers for image input.
    # ----------------------------------------------------------------

    def on_input_method_combobox_changed(self, widget, data=None):
        pass

    def on_address_search_button_clicked(self, widget, data=None):
        self.begin_search()

        self.model.address = self.view['address_entry'].get_text()
        self.view.set_status('Searching for location...')
        try:
            lat_long = self.run_search_from_address()
        except Exception, ex:
            self.view.set_status(str(ex), 'error')
            self.end_search()
        else:
            text = "Found: '%s'" % lat_long.address
            self.view.set_status(text, 'success')
            self.model.lat_long = lat_long

    def on_url_search_button_clicked(self, widget, data=None):
        self.begin_search()

        url = self.view['url_entry'].get_text()
        # Look for pano ID in URL.
        pano_id = extract_pano_id_from_url(url)
        if pano_id:
            self.model.pano_id = pano_id
        else:
            # No match for pano ID, look for co-ordinates.
            lat_long = extract_lat_long_from_url(url)
            if lat_long:
                self.model.lat_long = lat_long
            else:
                self.view.set_status(
                    'Could not extract pano ID or lat/long from URL.', 'error')
                self.end_search()

    def on_lat_long_search_button_clicked(self, widget, data=None):
        self.begin_search()

        latitude = parse_angle(self.view['latitude_entry'].get_text())
        longitude = parse_angle(self.view['longitude_entry'].get_text())

        if (latitude is None) or (longitude is None):
            if (latitude is None) and (longitude is None):
                self.view.set_status('Invalid latitude and longitude.', 'error')
            elif latitude is None:
                self.view.set_status('Invalid latitude.', 'error')
            elif longitude is None:
                self.view.set_status('Invalid longitude.', 'error')
            self.end_search()
        elif abs(latitude) > 90:
            self.view.set_status('Latitude cannot exceed 90.', 'error')
            self.end_search()
        elif abs(longitude) > 180:
            self.view.set_status('Longitude cannot exceed 180.', 'error')
            self.end_search()
        else:
            self.model.lat_long = gmaps.LatLng(latitude, longitude)

    def on_image_filechooserbutton_file_set(self, widget, data=None):
        self.begin_search()
        self.input_file = widget.get_filename()
        # Try to open file.
        self.end_search()

    def on_download_button_clicked(self, widget, data=None):
        details = self.model.pano_details
        zoom = self.view['resolution_combobox'].get_active()
        rows, cols = details.tilesAtZoom(zoom)
        # Set up table and create Downloader.
        self.view.create_download_table(rows, cols)
        download_progress = {}
        for y in range(rows):
            for x in range(cols):
                download_progress[(x, y)] = None
        self.model.download_progress = download_progress

        worker = downloader.PanoramaDownloaderSimple(details, zoom)
        worker.start()
        while not worker.downloaded:
            for y in range(rows):
                for x in range(cols):
                    progress = worker.get_progress((x, y))
                    self.model.download_progress[(x, y)] = progress
            service_gui()

        self.view.set_download_text("Compositing image...")
        while worker.isAlive():
            service_gui()
        image = worker.result
        self.model.pano = image

    def on_save_pano_button_clicked(self, widget, data=None):
        if not self.model.pano:
            return
        chooser = gtk.FileChooserDialog(
                title="Save Image As",
                action=gtk.FILE_CHOOSER_ACTION_SAVE,
                buttons=(
                    gtk.STOCK_SAVE, gtk.RESPONSE_OK,
                    gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
        chooser.set_do_overwrite_confirmation(True)
        saved = False
        cancelled = False
        while (not saved) and (not cancelled):
            result = chooser.run()
            filename = chooser.get_filename()
            if result in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
                print result
                cancelled = True
            else:
                try:
                    self.model.pano.save(filename)
                except KeyError:
                    # Invalid filename extension.
                    saved = False
                else:
                    saved = True
        chooser.destroy()

    # ----------------------------------------------------------------
    # Observable properties.
    # ----------------------------------------------------------------

    def property_lat_long_value_change(self, model, old, new):
        """Latitude/longitude value changed. Next stage is finding panorama."""
        if new is None:
            return

        self.view.set_status('Searching for nearest panorama...')
        try:
            pano_id = self.run_search_from_lat_long()
        except Exception, ex:
            self.view.set_status(str(ex), 'error')
            self.end_search()
        else:
            text = "Panorama ID: '%s'" % pano_id
            self.view.set_status(text, 'success')
            self.model.pano_id = pano_id

    def property_pano_id_value_change(self, model, old, new):
        """Latitude/longitude value changed. Next stage is finding panorama."""
        if new is None:
            return

        self.view.set_status('Retrieving panorama details...')
        try:
            pano_details = self.run_search_from_pano_id()
        except Exception, ex:
            self.view.set_status(str(ex), 'error')
            self.end_search()
        else:
            text = "Succeeded: '%s'" % pano_details.title
            self.view.set_status(text, 'success')
            self.model.pano_details = pano_details
            self.end_search()
            
    def property_pano_details_value_change(self, model, old, new):
        """Found the panorama. Set up next stage."""
        if new is None:
            return

        # List available resolutions and default to option 2.
        self.view.set_zoom_options(new.zoomLevelNames())
        self.view.set_zoom_selected(2)

    def property_download_progress_before_change(self, model,
            instance, name, args, kwargs):
        if name == '__setitem__':
            (x, y), progress = args
            if progress is None:
                fraction = 0.0
                text = ''
            else:
                fraction = progress.get_fraction()
                text = progress.text_percent()
            self.view.set_download_progress(x, y, fraction, text)

    def property_pano_value_change(self, model, old, new):
        if new is None:
            self.view.set_pano_frame_widget(None)
        else:
            self.view.set_pano_image(new)

    # ----------------------------------------------------------------
    # Other useful functions.
    # ----------------------------------------------------------------

    def begin_search(self):
        self.model.clear_search_data()
        self.view.clear_status()
        self.view.set_search_button_sensitive(False)

    def end_search(self):
        self.view.set_search_button_sensitive(True)

    def run_search_from_address(self):
        """Finds the latitude and longitude of an address.

        Throws:
        Exception if no address specified.
        GeocodeException if the address could not be found.

        Returns:
        A LatLng object.
        """
        # Check if address is empty.
        if (not self.model.address) or (len(self.model.address.strip()) == 0):
            raise Exception("No address specified!")
        # Search for location.
        worker = downloader.AddressFinder(key, self.model.address)
        worker.start()
        while worker.isAlive():
            # Update progress.
            #self.search_progressbar.update(worker.get_progress())
            # Stop the UI from becoming non-responsive.
            service_gui()
        #self.search_progressbar.update(worker.get_progress())
        # Succeeded?
        if worker.error:
            #self.search_textview.set_error(str(worker.error))
            raise worker.error
        return worker.result

    def run_search_from_lat_long(self):
        # Search for location.
        worker = downloader.PanoramaIDFinder(self.model.lat_long)
        worker.start()
        while worker.isAlive():
            # Update progress.
            #self.search_progressbar.update(worker.get_progress())
            # Stop the UI from becoming non-responsive.
            service_gui()
        #self.search_progressbar.update(worker.get_progress())
        # Succeeded?
        if worker.error:
            #self.search_textview.set_error(str(worker.error))
            raise worker.error
        return worker.result

    def run_search_from_pano_id(self):
        # Search for location.
        worker = downloader.PanoramaDetailsFinder(self.model.pano_id)
        worker.start()
        while worker.isAlive():
            # Update progress.
            #self.search_progressbar.update(worker.get_progress())
            # Stop the UI from becoming non-responsive.
            service_gui()
        #self.search_progressbar.update(worker.get_progress())
        # Succeeded?
        if worker.error:
            #self.search_textview.set_error(str(worker.error))
            raise worker.error
        return worker.result


